系列說明 >> 本系列大部分 PR 來自私人或公司專案,部分程式碼、環境設定與實作細節不便公開。
我只能保證文中提到的 PR 與問題皆為真實案例,但會經過必要的匿名化與內容調整。
本系列主要分享問題如何被發現、背後的思考方式,以及解法如何形成,不會深入討論完整實作與部署細節,敬請見諒。
Base Repo: paulsha-cortex
Change Ref: PR #1-feat: bootstrap paulsha-cortex 治理平面拆包
Issue: Deck 已經能把 Card/Combo 編成 dispatch: hold 的 Slice Specs,但誰判斷 Ready、誰 Dispatch、誰接住 Completion、誰把下一段放出去,以及 Operator 要怎麼送出 Retry/Pause/Resume,仍混在 paulshaclaw 的 Manager 與操作路徑裡。讀者看得到工作單,Runtime Responsibility 卻還沒有清楚分層。
Root Cause: 「工作怎麼定義」、「工作怎麼往下走」與「人怎麼介入」被放在同一個 Repo、同一組路徑與共用 Helper 裡。Coordinator/Control 的責任雖然已經存在,卻沒有獨立的 Runtime Boundary;只 Copy Source,Path、Phase/Idle、CLI、Service 與舊 memory 模組依賴仍會把新 Repo 拉回舊環境。
Solution: 先把 Coordinator 與 Control 的責任切清楚:Coordinator 掃描 Specs、判 Ready、Dispatch、接 Completion/Handoff 並 Release 下游;Control 提供 Operator Request/Runtime Status 的正式交換面。接著建立 paulsha-cortex,平移 persona / coordinator / control,補齊自己的 Path Contract、Phase/Idle 定義、CLI、Hooks、Service/Installer 與受限 Runtime Config Parser,剪除對舊 memory 模組提供之基本 Runtime 定義的直接依賴。
Evidence: PR #1 完成 Tests、Policy Check、去識別化掃描與 Fresh-install Smoke;pip install . 後 cortex --help Exit 0,核心執行對舊 memory Namespace 的直接依賴清零。這些證據支持 Persona/Coordinator/Control 已形成可獨立安裝與啟動的 Runtime Nucleus;完整 Package Cutover 與舊 Manager 退役仍留待下一步。
上一篇,Deck 把 feature-oneshot 編成五份 Specs。
build dispatch: hold
code-review dispatch: hold
verification dispatch: hold
ship dispatch: hold
adversarial-review dispatch: hold
Dependency 也寫好了。
Build 沒完成,Review 不該開始;Review 沒完成,Verification 先等著。
很好。
這次總算不用我站在十個小bu旁邊,一個一個提醒下一步。
小bu看著五份工作單。
「都生出來了,我先把
hold全改成auto?」
我差點答應。
反正 Dependency 已經寫在裡面,先全部放出去,該等的自然會等。
小ma抬頭。
「還沒。」
「又怎麼了?」
「誰確認上游真的完成?」
「誰接它交回來的 Artifact?」
「誰決定下一份現在可以放?」
小bu覺得這些都是小事。
「我可以在每份 Spec 後面補一個 Script。」
小re接得很快:
「那五份工作,是不是就要各自長出一小段 Manager?」
……好。
牌是寫好了。
但牌不會自己知道現在輪到哪一張。
前幾天,我們一路補過:
poll
→ gate
→ handoff
→ release downstream
故事裡的小ma,偏見一直很單純:
單一步驟做完,不代表整個 Lifecycle 可以往下。
但小ma不是 Source Tree 裡的一個 Package。
回到 Code,真正會掃描 Specs、計算 Ready Unit、Dispatch Worker、接住 Completion,再把下游放出去的,是 coordinator。
為了確認我沒有又把一個名詞想得太玄,我乾脆手動演了一次。
先把 Build 翻成 auto。
等它完成。
看 Handoff。
再去把 Review 放出去。
接著等 Review。
再放 Verification。
做到第三張時,老Go問:
「你現在是在驗證 Coordinator,還是親自應徵 Coordinator?」
我看著手上的終端機。
確實。
Deck 幫我省掉了手寫五份工作單。
我卻又把自己補回去,負責逐張看、逐張放、逐張收。
所以 Coordinator 的責任不是「另一個會寫 Code 的 Agent」。
它是在工作已經被定義之後,持續回答:
現在誰 Ready?
誰還得等?
完成的 Artifact 在哪裡?
下一個 Transition 能不能發生?
小pa聽到這裡,順手把界線補了一刀:
「Plan 已經告訴你這次要做什麼。」
「不要每次 Runtime 卡住,又回來叫 Planner 重寫一份 Plan。」
嗯。
角色終於開始各做各的事了。
Coordinator 開始像樣之後,我又碰到另一個很現場的問題。
假設 Verification 卡住,我想:
暫停這個 Job。
Retry 一次。
改成人工接手。
回答一個 Blocking Decision。
我第一個反應,是直接改 Spec 或 Status File。
小bu覺得很合理。
「反正都是 YAML/JSON,改完就生效。」
小re看了一眼:
「你現在改的是工作定義,還是在下操作命令?」
「……操作命令。」
「那為什麼寫回工作定義?」
如果我為了 Retry,直接去改 Deck 生出的 Spec,下一次就很難分辨:
這份 Workflow 本來就是這樣
還是 Operator 剛才臨時介入
反過來,如果人直接去摸 Coordinator 的 Internal State,那就更精彩了。
Coordinator 以為自己持有正式狀態。
Operator 也覺得檔案是我改的,當然算正式。
兩邊都很有 Authority。
通常只差一個晚上,就會開始互相覆蓋。
所以需要另一個邊界:Control。
Operator
→ 寫 Request
Coordinator
→ 讀 Request、執行 Transition
Coordinator
→ 寫 Status
Operator
→ 讀 Status
一句話說:
Coordinator 負責讓工作往下走;Control 負責讓人跟 Runtime 對同一份 Request/Status 說話。
老Go聽完點點頭。
「方向盤可以留給人。」
「不要每次轉彎,都把手伸進齒輪箱裡撥。」
這個比喻很土。
也很難反駁。
到這裡,前幾天陸續出場的責任開始對得起來了:
Persona
→ Agent 以什麼角色與 Guardrail 做事
Deck
→ 可重用工作怎麼宣告與編譯
Coordinator
→ 工作如何依 Dependency 往下走
Control
→ Operator 與 Runtime 如何交換 Request/Status
而我拿來操作這些東西的 CLI、Cockpit、Start Script,也都在 paulshaclaw。
換句話說,一邊是我伸手操作 Agent 的入口。
另一邊已經開始持有 Workflow 的正式狀態與推進責任。
兩邊都住在同一個 Repo。
小bu提出第一個方案:
「加幾條 Import Rule,把目錄分乾淨就好。」
我也很想選這條。
不用搬 Repo。
不用改 Install。
不用處理 Service。
大家各住各的資料夾,平常不要越界,看起來十分和平。
老Go問:
「你現在是靠 Boundary 讓它們分開。」
「還是靠 Test 每天提醒它們假裝分開?」
這句有點痛。
我打開 Repo Tree:
paulshaclaw/
├── persona/
├── coordinator/
├── control/
└── cli/
前三個已經是 Runtime Nucleus。
最後一個才是 Operator 入口。
所以我採用了工程師最熟悉的架構拆分方法:
cp -r paulshaclaw/persona paulsha-cortex/
cp -r paulshaclaw/coordinator paulsha-cortex/
cp -r paulshaclaw/control paulsha-cortex/
三個資料夾出現在新 Repo。
小bu宣布:
「搬完。」
小re沒有看 Git Status。
「把舊 Repo 從
PYTHONPATH拿掉,再跑一次。」
群裡安靜了兩秒。
第一個撞牆的是 Hook Path。
我站在 Cortex Repo Root 手動執行,PASS。
真正從 Target Worktree 被叫起來,相對路徑立刻指到另一棵樹,Script 找不到 Runtime。
小bu抗議:
「我剛才明明跑過。」
小re問:
「你測的是 Repo Root,還是實際 Caller 的 Working Directory?」
「Repo Root。」
「那你測到的是你站的位置很正確。」
很好。
家具到了新家,地址簿還留在舊家。
接著是另一條 Dependency。
當時 paulshaclaw 裡還有一組整理 Agent 工程記憶的 memory 模組,細節後面再講。
今天只需要知道:Coordinator 要判斷 Phase、Idle 與部分 Runtime Path,竟然還得先去 Import 那組 memory Helper。
小re乾脆把它拿掉。
「現在 Cortex 還起得來嗎?」
起不來。
老Go看著 Traceback:
「記憶可以晚點載入。」
「心跳不能等記憶系統到場才開始。」
所以 PR #1 真正補的,不只是三個 Package:
自己的 Path Contract
自己的 Phase/Idle 定義
自己的 Control 基本契約
自己的 CLI、Hook、Service 與 Installer
Cortex 甚至必須能在 Target Environment 半壞、Virtualenv 不完整時讀懂自己的最低限度 Config;因此 Runtime 只內建契約真正需要的受限格式,遇到不支援內容就直接拒絕。
這時我才承認:
Source Boundary 只是檔案放哪裡。
Runtime Boundary 是拿掉舊家之後,它還知不知道自己怎麼活。
cortex --help 叫得出來,只證明 Coordinator 終於有自己的門牌最後做 Fresh-install Smoke:
pip install .
cortex --help
# exit 0
加上 Tests、Policy Check、去識別化掃描,以及對舊 memory Namespace 的直接依賴清理,這些 Evidence 可以支持:
Persona/Coordinator/Control 已經在 Cortex 形成可獨立安裝的 Runtime Nucleus。
Coordinator 有自己的 Path、Phase/Idle、CLI、Hook 與 Service 啟動面。
Operator 不必再透過舊 Repo,才能叫出新 Runtime。
它不能支持:
整套 Cortex 已經拆完,paulshaclaw 從此只剩 Shell。
因為還有 Runtime 能力留在舊 Repo。
更麻煩的是,舊 Manager Path 也還沒有正式退役。
我第一個想法仍然很保守:
新舊兩邊先一起留著,不是比較方便 Rollback 嗎?
小bu立刻同意。
「安全。」
小re問:
「同一份 Request,兩邊都看到時,哪一份 Status 算真的?」
小ma看了一眼舊 Timer,又看了一眼新 Service。
「還沒。」
「這次還缺什麼?」
老Go替它回答:
「你現在不是留了備份。」
「你是留了兩個人,都能踩同一台車的油門。」
下一篇,Cortex 已經能跑,舊 Manager 卻還沒下班。
Have a nice day.